County-Wide Exemption Summary Statistics (Tax Year 2021)
Code
year_examples <-c(2021)for(i in year_examples){ tbl <- muni_cl_sums |>filter(year == i) |>group_by(year) |>summarize(AV =sum(muni_c_av), EAV =sum(muni_c_eav), Eq_AV =sum(muni_c_equalized_av),'Taxed EAV'=sum(muni_c_current_taxable_eav),'All Exemptions'=sum(muni_c_all_exemptions), 'GHE'=sum(muni_c_exe_homeowner), 'Senior Exemp.'=sum(muni_c_exe_senior), 'Freeze Exemp.'=sum(muni_c_exe_freeze), 'PINs in Muni'=sum(muni_c_pins_in_muni),'PINs with Exemptions'=sum(muni_c_has_HO_exemp)) |>pivot_longer(cols =c(AV:'PINs with Exemptions'), names_to ="Totals", values_to ="Values")}tbl |> flextable::flextable()
year
Totals
Values
2,021
AV
70,733,758,749
2,021
EAV
212,387,387,384
2,021
Eq_AV
212,392,258,554
2,021
Taxed EAV
175,164,233,392
2,021
All Exemptions
16,110,627,371
2,021
GHE
10,161,823,944
2,021
Senior Exemp.
2,627,007,663
2,021
Freeze Exemp.
3,229,599,122
2,021
PINs in Muni
1,864,594
2,021
PINs with Exemptions
1,028,964
Table 1.1
Eq_AV is the 2021 equalization factor multiplied by the assessed value of all property and can be thought of as “EAV pre-exemption.” The general homestead exemption accounts for the vast majority of exempt EAV.
Figure 3.
Code
year_examples <-c(2021)for(i in year_examples){median_exempt <- muni_sums |>filter(year==i)|>mutate(pct_fmv_exempt = muni_fmv_exempt / muni_fmv) |>select(pct_fmv_exempt)midpoint = scales::percent(median(median_exempt$pct_fmv_exempt), accuracy =0.01)print(muni_sums |>filter(year==i)|>mutate(pct_fmv_exempt = muni_fmv_exempt / muni_fmv ) |>mutate(agency_name =ifelse(agency_name =="TOWN CICERO", "CITY OF CICERO", agency_name) ) |>full_join(muni_shp, by =c("agency_name"="AGENCY_DESC")) |>ggplot(aes(fill = pct_fmv_exempt)) +geom_sf(aes(geometry = geometry), color ="black") +theme_void()+labs(title =paste0(i), subtitle ="Exempt FMV as a Percent of Municipality-Wide FMV",caption =sprintf("The County-wide municipal-median is %s", midpoint)) +theme_void() +theme(axis.ticks =element_blank(), axis.text =element_blank())+scale_fill_steps2(high ="darkblue", low ="black", mid ="beige",n.breaks =7, show.limits=TRUE,na.value =NA,nice.breaks =FALSE,midpoint =median(median_exempt$pct_fmv_exempt),name ="% Exempt", label = scales::percent))}
Code
year_examples <-c(2021)for(i in year_examples){median_exempt <- muni_sums |>filter(year==i)|>mutate(pct_fmv_exempt = muni_fmv_exempt / muni_fmv) |>select(pct_fmv_exempt)midpoint = scales::percent(median(median_exempt$pct_fmv_exempt), accuracy =0.01)print(muni_sums |>filter(year==i)|>mutate(pct_fmv_exempt = muni_fmv_exempt / muni_fmv ) |>mutate(agency_name =ifelse(agency_name =="TOWN CICERO", "CITY OF CICERO", agency_name) ) |>full_join(muni_shp, by =c("agency_name"="AGENCY_DESC")) |>ggplot(aes(fill = pct_fmv_exempt)) +geom_sf(aes(geometry = geometry), color ="black") +theme_void()+labs(title =paste0(i), subtitle ="Exempt FMV as a Percent of Municipality-Wide FMV"#,#caption = sprintf("The County-wide municipal-median is %s", midpoint) ) +theme(axis.ticks =element_blank(), axis.text =element_blank())+scale_fill_steps(high ="darkblue", low ="pink",n.breaks =5, show.limits=TRUE,limits =c(0, 0.35),na.value ="gray50",nice.breaks =FALSE,name ="% Exempt", label = scales::percent))}
The County-wide municipal-median percent of FMV exempt from property taxes in 2021 was 14.40% (Muni Name).
The percent of municipal EAV exempt from property taxes are greatest across Cook County’s south suburbs and lowest in the northermost suburbs of the county.
Table 1.2: Exempt EAV in Cook County, Tax Year 2021 Exemption use varies between suburban Cook County ($9.7B EAV) and the City of Chicago ($7.0B EAV), which may point to broader socioeconomic patterns as well as residents’ eligibility and update. Note: Our current calculations undervalue the disabled veterans exemption.
Exemption use varies between suburban Cook County ($9.7B EAV) and the City of Chicago ($7.0B EAV), which may point to broader socioeconomic patterns as well as residents’ eligibility and update.
Figure 5. Value of residential exemptions by type in Cook County over time, Tax Years 2006 - 2021
Trends in the total EAV reductions due to homestead exemptions reflect both real estate market shifts and the legislative changes in their availability, eligibility, and value.
Effect on Composite Tax Rates
NOTE: I THINK WE WANT THIS CHUNK ON THE DATA PAGE
Code
cross_county_lines <-c("030440000", "030585000", "030890000", "030320000", "031280000","030080000", "030560000", "031120000", "030280000", "030340000","030150000","030050000", "030180000","030500000", "031210000")eq_factor <-3.0027incentive_majorclasses <-c("6", "7A", "7B", "8A", "8B")commercial_classes <-c(401:435, 490, 491, 492, 496:499,500:535,590, 591, 592, 597:599, 700:799,800:835, 891, 892, 897, 899) industrial_classes <-c(480:489,493, 550:589, 593,600:699,850:890, 893 )ptax_pins <-read_csv("../Output/Dont_Upload/0_Joined_PIN_data_2023.csv") |>select(-c(eq_av, propclass_1dig))ptax_pins <- ptax_pins |>mutate(class_1dig =str_sub(class, 1,1),class_group =case_when( (class_1dig ==5& class %in% commercial_classes) ~"5A", (class_1dig ==5& class %in% industrial_classes) ~"5B", class_1dig ==7& class <742~"7A", class_1dig ==7& class >=742~"7B", (class_1dig ==8& class %in% commercial_classes ) ~"8A", (class_1dig ==8& class %in% industrial_classes ) ~"8B",TRUE~as.character(class_1dig))) |>mutate(# taxing district revenue = taxable eav * tax rate so rearrange the formula:taxed_eav = final_tax_to_dist / tax_code_rate*100,total_value_eav = (final_tax_to_dist + final_tax_to_tif)/ tax_code_rate *100+ all_exemptions + abatements,exempt_eav_inTIF =ifelse(in_tif ==1, all_exemptions, 0),exempt_eav = all_exemptions + abatements,taxed_av = taxed_eav / eq_factor, # current value that taxing agencies can tax for their levies## taxable AV = equalized assessed value net TIF increments, gross exemptions. ## Used for calculating untaxable value further below# taxable_av = (final_tax_to_dist / tax_code_rate *100 + all_exemptions + abatements)/ eq_factor, # taxable_eav_fromincents = ifelse(class >=600 & class < 900, taxable_av * eq_factor, 0),## untaxable value = exempt EAV from abatements and exemptions + TIF incrementuntaxable_value_eav = all_exemptions + abatements +## TIF increment EAV above frozen EAV, which becomes TIF revenue (final_tax_to_tif / tax_code_rate*100) +## difference between 25% and reduced level of assessment for incentive class properties. Excludes TIF increment when calculating the difference! ifelse(between(class, 600, 899), (taxed_av/loa*0.25- taxed_av)*eq_factor, 0),untaxable_incent_eav =ifelse(between(class, 600, 899), (taxed_av/loa*0.25- taxed_av)*eq_factor, 0),# Class 239 property values (farms) are unequalized and reflect different farm# valuations (See CCAO Classification Document)untaxable_value_eav =ifelse(class ==239, equalized_av-taxed_eav, untaxable_value_eav), untaxable_value_av = untaxable_value_eav / eq_factor,untaxable_value_fmv = untaxable_value_av / loa,exempt_fmv = exempt_eav / eq_factor / loa, fmv_inTIF =ifelse(in_tif==1, av/loa, 0),fmv_tif_increment =ifelse(final_tax_to_tif >0, ((final_tax_to_tif / (tax_code_rate/100)) / eq_factor ) / loa, 0),fmv_incents_inTIF =ifelse(between(class, 600, 899) & in_tif ==1, fmv, 0),fmv_incents_tif_increment =ifelse(between(class, 600, 899) & final_tax_to_tif >0 , ((final_tax_to_tif / (tax_code_rate/100)) / eq_factor ) / loa, 0),naive_rev_forgone = untaxable_incent_eav * tax_code_rate/100) |>select(tax_code, class, pin, fmv, untaxable_value_fmv, fmv_inTIF, fmv_tif_increment, fmv, total_billed, final_tax_to_dist, final_tax_to_tif, tax_code_rate, eav, equalized_av, av, everything())
Code
muni_ratechange <- ptax_pins |>mutate(class =as.numeric(class)) |># Allows for joining later# select(-c(propclass_1dig:av.y)) |>filter(!clean_name %in%c("Frankfort", "Homer Glen", "Oak Brook", "East Dundee", "University Park", "Bensenville", "Hinsdale", "Roselle", "Deer Park", "Deerfield")) |># filter(!agency_num %in% cross_county_lines) |>group_by(clean_name) |>summarize(classgroup_PC =n(),# projects = n_distinct(both_ids), # mostly for industrial and commercial propertiespins_withincents =sum(ifelse(class >=600& class <900, 1,0)),fmv_incentive =sum(ifelse(class >=600& class <900, fmv, 0), na.rm =TRUE),#fmv_taxed = sum(taxed_fmv, na.rm=TRUE),fmv_incents_inTIFs =sum(ifelse(class >=600& class <900& final_tax_to_tif >0, fmv, 0), na.rm =TRUE),fmv_inTIF =sum(ifelse(final_tax_to_tif >0, fmv, 0), na.rm=TRUE),fmv_tif_increment =sum(fmv_tif_increment, na.rm=TRUE),fmv_untaxable_value =sum(untaxable_value_fmv , na.rm=TRUE),fmv_exemptions =sum(all_exemptions/eq_factor/loa, na.rm=TRUE),fmv_abatements =sum(exe_abate/eq_factor/loa, na.rm=TRUE),zero_bill =sum(zero_bill, na.rm=TRUE),fmv_residential =sum(ifelse(class %in%c(200:399), fmv, 0), na.rm =TRUE),fmv_C2 =sum(ifelse(class %in%c(200:299), fmv, 0), na.rm =TRUE),fmv_industrial =sum(ifelse(class %in% industrial_classes, fmv, 0), na.rm =TRUE),fmv_commercial =sum(ifelse(class %in% commercial_classes, fmv, 0), na.rm =TRUE),current_rate_avg =mean(tax_code_rate),avg_C2_bill_noexe =mean(ifelse(between(class,200,299) & all_exemptions ==0, (final_tax_to_dist + final_tax_to_tif), NA), na.rm=TRUE),avg_C2_bill_withexe =mean(ifelse(between(class,200,299) & all_exemptions >0, (final_tax_to_dist + final_tax_to_tif), NA), na.rm=TRUE),av_taxed =sum(taxed_av, na.rm =TRUE),untaxable_value_av =sum(untaxable_value_av, na.rm=TRUE),av =sum(av),eav_taxed =sum(taxed_av*eq_factor), eav_untaxable =sum(untaxable_value_eav, na.rm=TRUE),eav_tif_increment =sum(final_tax_to_tif/tax_code_rate, na.rm=TRUE),eav_max =sum(fmv*loa*eq_factor, na.rm=TRUE),fmv =sum(fmv, na.rm=TRUE),pins_in_class =n(),all_exemptions =sum(all_exemptions), # in EAVabatements =sum(exe_abate), # in EAVeav_incents_inTIFs =sum(ifelse(class >=600& class <=900& in_tif ==1, eav, 0), na.rm =TRUE),final_tax_to_dist =sum(final_tax_to_dist),final_tax_to_tif =sum(final_tax_to_tif),eav =sum(eav),new_TEAV_noIncents =sum(ifelse(class >=600& class <900, (taxed_av*eq_factor/loa)*0.25, taxed_av*eq_factor), na.rm=TRUE),####### Not used currently# new_TEAV_noC6 = sum(ifelse( class >=600 & class <700, # (taxed_av*eq_factor/loa)*0.25 , taxed_av*eq_factor)),# new_TEAV_noC7 = sum(ifelse(class >=700 & class <800,# (taxed_av*eq_factor/loa)*0.25, taxed_av*eq_factor)),# new_TEAV_noC8 = sum(ifelse(class >=800 & class <900, (taxed_av*eq_factor/loa)*0.25, taxed_av*eq_factor)),# #######new_TEAV_vacant_noIncents =sum(ifelse(class >=600& class <900,0, taxed_av*eq_factor)) ) |>mutate(new_TEAV_noExemps = eav_taxed + all_exemptions, # does not include abatementsnew_TEAV_noAbates = eav_taxed + abatements, # include only abatements, not other exemption types# amount of EAV from taxing an additional 15% of the AV if incentive properties didn't existforgone_EAV_incent =#class_group %in% incentive_majorclasses,#incent_prop == "Incentive", new_TEAV_noIncents - eav_taxed) |>#cbind(table_cook) |>mutate(# Absolute maximum TEAV: No Exemptions, no abatements, no TIFS, no Incentive properties# Commercial and industrial assessed at 25%TEAV_max = eav_taxed + all_exemptions + abatements + eav_tif_increment + forgone_EAV_incent,# no exemptions or incentive classifications:TEAV_neither = eav_taxed + all_exemptions + forgone_EAV_incent,rate_noExe = final_tax_to_dist / new_TEAV_noExemps *100,rate_noAbate = final_tax_to_dist / new_TEAV_noAbates *100,rate_noInc = final_tax_to_dist / new_TEAV_noIncents *100,rate_neither = final_tax_to_dist / TEAV_neither *100, rate_noTIFs = final_tax_to_dist / (eav_taxed + eav_tif_increment) *100,rate_vacant = final_tax_to_dist / new_TEAV_vacant_noIncents*100,rate_lowest = final_tax_to_dist / TEAV_max *100,# rate_noC6 = levy / new_TEAV_noC6 * 100,# rate_noC7 = levy / TEAV_noC7 * 100,# rate_noC8 = levy / TEAV_noC8 * 100,rate_current = final_tax_to_dist / eav_taxed *100,change_noInc = rate_current - rate_noInc,change_neither = rate_current - rate_neither,change_noTIF = rate_current - rate_noTIFs,change_noExe = rate_current - rate_noExe,change_vacant = rate_current - rate_vacant,change_lowest = rate_current - rate_lowest ) |>mutate(across(contains("rate_"), round, digits =2)) |>mutate(across(contains("change_"), round, digits =2))write_csv(muni_ratechange, "../Output/muni_ratechange_2023.csv")
Table 2: Change in the share of property tax burden due to exemptions for single-family, multi-family, and commercial and industrial properties, tax year 2023
Code
burden_shift <- ptax_pins |>mutate(Group =case_when( class_1dig ==2~"Single-family", class_1dig %in%c(3, 9) ~"Multi-family",TRUE~"Commercial & Industrial" )) |>left_join(taxcode_taxrates, by ="tax_code") |>group_by(clean_name, Group) |>## calculate taxbase from each major class ## and the amount of taxes currently collected from eachsummarize(group_taxbase=sum(taxed_eav, na.rm = T),group_taxes_current =sum(taxed_eav * (tax_code_rate/100), na.rm = T),hyp_group_taxbase =sum(taxed_eav + all_exemptions, na.rm = T),hyp_group_taxes =sum( (taxed_eav + all_exemptions) * (new_comp_TC_rate/100), na.rm = T) ) |>mutate(across(c(group_taxbase:hyp_group_taxes), round, 0)) |>ungroup() |>group_by(clean_name) |>mutate(muni_taxbase =sum(group_taxbase, na.rm=T),muni_levy =sum(group_taxes_current, na.rm = T) ) |>ungroup() |>mutate(pct_eav_current = group_taxbase / muni_taxbase,pct_taxburden_current = group_taxes_current / muni_levy,hyp_pct_taxburden = hyp_group_taxes / muni_levy) |>mutate(burden_shift = (pct_taxburden_current - hyp_pct_taxburden)*100)
## Grouped by if they have a $0 tax bill and had the GHE per muni## Recalculating for Josh & Rachaelmuni_median_summarytable <- ptax_pins |>filter(class >199& class <300) |># merge in muni residential median AVleft_join(C2_munistats_filtered) |># +/- 500 from municpalities median residential AVfilter(av < median_av+200& av > median_av-200) |># Removes properties that received other types of exemptionsfilter(exe_senior ==0& exe_freeze ==0& exe_longtime_homeowner ==0& exe_disabled ==0& exe_vet_returning ==0& exe_vet_dis_lt50 ==0& exe_vet_dis_50_69 ==0& exe_vet_dis_ge70 ==0& exe_abate ==0) |>arrange(av) |>mutate(bill_current = (final_tax_to_dist + final_tax_to_tif),bill_noexemps = new_comp_TC_rate/100*(equalized_av-all_exemptions+exe_homeowner),bill_change = bill_noexemps - bill_current) |>group_by(clean_name, # zero_bill, has_HO_exemp) |>summarize(AV =median(median_av), # median_av was calculated earlier: C2 median AV for the muni `Taxable EAV`=round(median(eav)),bill_cur =round(median(bill_current)),bill_new =round(median(bill_noexemps)),bill_change =round(median(bill_change)),pincount=n(),perceived_savings =round(median(tax_amt_exe))) |># merge in clean_names variableleft_join(nicknames) |>select(clean_name, has_HO_exemp, bill_cur, bill_new, bill_change, perceived_savings, AV, `Taxable EAV`, everything()) |>select(-c(agency_number, agency_name))muni_median_summarytable
Municipalities with the largest and smallest reductions in tax base (as a share of residential EAV due to exemptions) and median property values
Figure 10. Tax Burden Shift from Current GHE
Share of municipal property tax levy paid by Class 2 properties with and without homestead exemptions, tax year 2021
Code
# as a dot graph ## order <- mc_burden |>filter(class_1dig ==2) |>select(clean_name, pct_taxburden_current, burden_shift)slice <- mc_burden |>filter(class_1dig ==2) |>select(clean_name, pct_taxburden_current, burden_shift) |>arrange(pct_taxburden_current) |>slice(1:5, 63:67, 127:131)median_burden <-median(order$pct_taxburden_current)median_shift <-median(order$burden_shift)# median burden change is 5.9 percentage points# current median burden is 70.3% of the levymc_burden |>filter(clean_name %in% slice$clean_name) |>#filter(!clean_name %in% cross_county_lines$clean_name)|>filter(class_1dig ==2) |># filter(burden_current > 0.938 |burden_current < .17 |# ( (burden_current < median(burden_current) + 0.01 )& (burden_current > median(burden_current) - 0.01)) )|> ungroup() |>select(clean_name, pct_taxburden_current, hyp_pct_taxburden, burden_shift) |>arrange(burden_shift) |># mutate( burden_noexemps = ifelse(burden_noexemps > 1, 1, burden_noexemps)) |>pivot_longer(c("pct_taxburden_current", "hyp_pct_taxburden"), names_to ="type", values_to ="pct_burden") |>inner_join(order) |>ggplot(aes(x = pct_burden*100, y=reorder(clean_name, -pct_taxburden_current)))+# y= reorder(clean_name, burden_current)))+geom_vline(xintercept =70.2, linetype =3)+geom_line(aes(group = clean_name))+geom_hline(yintercept =5.5, linetype =2)+geom_hline(yintercept =10.5, linetype =2)+geom_point(aes(color=type), size=3 )+theme_minimal() +theme(#legend.position = "none", legend.title =element_blank(),plot.title.position ="plot",# panel.background = element_rect(fill='transparent'), #transparent panel bgplot.background =element_rect(fill='transparent', color=NA) #transparent plot bg )+scale_color_brewer(palette="Paired", labels =c("Current Burden", "Burden if \nNo Exemptions" ), direction =1)+labs(title ="Change in Class 2 Residential Tax Burden", subtitle ="Ordered by Current Tax Burden",x ="Share of Levy (%)", y ="" , caption ="Dotted line represents median Class 2 burden (65.5% of the levy). Residential Tax Burden is the share of the property tax collected that was paid for by property owners with Class 2 properties.") +geom_label(label ="Class 2 pays small share of \nlevy; very little residential", x=32, y =13, label.size =1, size =3)+geom_label(label ="Class 2 pays median share of \nlevy (70.3%), mix of land use", x=42, y =7.5, label.size =1, size =3) +geom_label(label ="Class 2 pays nearly all of levy, \nhighly residential", x=70, y =3, label.size =1,size =3)
Figure 1.1
Figure 11. Zero Dollar Bills
Code
muni_mc_sums |>filter(major_class_code ==2) |>group_by(year) |>summarize(zerodollar_count =sum(zero_bill)) |>ggplot(aes(x=year, y = zerodollar_count)) +geom_bar(position ="stack", stat ="identity") +theme_minimal()